﻿using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace laohan.Common.USB
{
    public class USBDevice : IDisposable
    {
        public delegate void DataReceiveHandler(byte[] data);

        /// <summary>
        /// 
        /// </summary>
        public event EventHandler<LogEventArgs> LogEvent;

        private int _vid;

        private int _pid;

        private static Guid device_class;

        private FileStream _usbCacheFile;

        private int _usbInputReportLength;

        private int _usbOutputReportLength;

        private IntPtr _usbhFileHandle;

        private IntPtr _lptemp = new IntPtr(1024);

        private IntPtr NullHandle = IntPtr.Zero;

        private IntPtr InvalidHandleValue = new IntPtr(-1);

        private string _usbPath = string.Empty;

        public bool IsOpen;

        public event DataReceiveHandler OnUsbDataReceived;

        public USBDevice(int VID, int PID)
        {
            _vid = VID;
            _pid = PID;
            SearchDevice();
        }

        public void Write(byte[] data)
        {
            try
            {
                if (_usbCacheFile != null && _usbCacheFile.CanWrite)
                {
                    _usbCacheFile.Write(data, 0, data.Length);
                }
            }
            catch (Exception ex)
            {
                WriteLog(string.Format(@"Send failed msg: {0}", ex.Message));
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="msg"></param>
        public void WriteLog(string msg, bool isError = false)
        {
            this.LogEvent?.Invoke(this, new LogEventArgs()
            {
                Msg = msg,
                IsError = isError
            });
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="error"></param>
        public void WriteError(string error)
        {
            WriteLog(error, true);
        }

        public void Close()
        {
            Dispose();
        }

        public void Dispose()
        {
            Dispose(bDisposing: true);
            GC.SuppressFinalize(this);
        }

        private void SearchDevice()
        {
            string value = $"vid_{_vid:x4}&pid_{_pid:x4}";
            Guid gClass = (device_class = Win32API.HIDGuid);
            IntPtr intPtr = Win32API.SetupDiGetClassDevs(ref gClass, null, IntPtr.Zero, 18u);
            try
            {
                Win32API.DeviceInterfaceData oInterfaceData = default(Win32API.DeviceInterfaceData);
                oInterfaceData.Size = Marshal.SizeOf(oInterfaceData);
                for (int i = 0; Win32API.SetupDiEnumDeviceInterfaces(intPtr, 0u, ref gClass, (uint)i, ref oInterfaceData); i++)
                {
                    string devicePath = GetDevicePath(intPtr, ref oInterfaceData);
                    if (devicePath.Contains(value))
                    {
                        _usbPath = devicePath;
                    }
                }
            }
            catch (Exception ex)
            {
                WriteError(ex.Message);
            }
            finally
            {
                Win32API.SetupDiDestroyDeviceInfoList(intPtr);
            }
        }

        private string GetDevicePath(IntPtr hInfoSet, ref Win32API.DeviceInterfaceData oInterface)
        {
            uint nRequiredSize = 0u;
            if (!Win32API.SetupDiGetDeviceInterfaceDetail(hInfoSet, ref oInterface, IntPtr.Zero, 0u, ref nRequiredSize, IntPtr.Zero))
            {
                Win32API.DeviceInterfaceDetailData oDetailData = default(Win32API.DeviceInterfaceDetailData);
                if (Marshal.SizeOf(typeof(IntPtr)) == 8)
                {
                    oDetailData.Size = 8;
                }
                else
                {
                    oDetailData.Size = 5;
                }
                if (Win32API.SetupDiGetDeviceInterfaceDetail(hInfoSet, ref oInterface, ref oDetailData, nRequiredSize, ref nRequiredSize, IntPtr.Zero))
                {
                    return oDetailData.DevicePath;
                }
            }
            return string.Empty;
        }

        public void Open()
        {
            if (string.IsNullOrEmpty(_usbPath))
            {
                IsOpen = false;
                return;
            }
            _usbhFileHandle = Win32API.CreateFile(_usbPath, 3221225472u, 3u, IntPtr.Zero, 4u, 1073741824u, _lptemp);
            IntPtr lpData;
            if (!(_usbhFileHandle != InvalidHandleValue))
            {
                _ = _usbhFileHandle;
                IsOpen = false;
                _usbhFileHandle = IntPtr.Zero;

                WriteError("Failed to create device file");
            }
            else if (Win32API.HidD_GetPreparsedData(_usbhFileHandle, out lpData))
            {
                try
                {
                    Win32API.HidP_GetCaps(lpData, out var oCaps);
                    _usbInputReportLength = oCaps.InputReportByteLength;
                    _usbOutputReportLength = oCaps.OutputReportByteLength;
                    _usbCacheFile = new FileStream(new SafeFileHandle(_usbhFileHandle, ownsHandle: false), FileAccess.ReadWrite, _usbInputReportLength, isAsync: true);
                    BeginAsyncRead();
                }
                catch (Exception ex)
                {
                    WriteError(string.Format("Failed to get the detailed data from the hid. error:{0}", ex.Message));
                }
                finally
                {
                    Win32API.HidD_FreePreparsedData(ref lpData);
                }
            }
            else
            {
                IsOpen = false;
                WriteError("GetPreparsedData failed Error:{0}");
            }
        }

        private void BeginAsyncRead()
        {
            byte[] array = new byte[_usbInputReportLength];
            if (_usbCacheFile != null)
            {
                _usbCacheFile.BeginRead(array, 0, _usbInputReportLength, EndAsyncRead, array);
                IsOpen = true;
            }
            else
            {
                IsOpen = false;
            }
        }

        private void EndAsyncRead(IAsyncResult iResult)
        {
            byte[] src = (byte[])iResult.AsyncState;
            try
            {
                if (_usbCacheFile != null)
                {
                    _usbCacheFile.EndRead(iResult);
                }
                if (this.OnUsbDataReceived != null)
                {
                    byte[] array = new byte[_usbInputReportLength - 1];
                    Buffer.BlockCopy(src, 1, array, 0, array.Length);
                    this.OnUsbDataReceived(array);
                }
                BeginAsyncRead();
            }
            catch (IOException ex)
            {
                WriteError("EndAsyncRead failed msg: " + ex.Message);
                Dispose();
            }
        }

        protected virtual void Dispose(bool bDisposing)
        {
            IsOpen = false;
            try
            {
                if (bDisposing)
                {
                    if (_usbCacheFile != null)
                    {
                        _usbCacheFile.Close();
                    }
                    _usbCacheFile = null;
                }
                if (_usbhFileHandle != IntPtr.Zero)
                {
                    Win32API.CloseHandle(_usbhFileHandle);
                    _usbhFileHandle = IntPtr.Zero;
                }
            }
            catch (Exception ex)
            {
                WriteError(ex.ToString());
            }
        }
    }
}
